iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 17
2
Mobile Development

Android TDD 測試驅動開發系列 第 17

Day17 - Android MVP 架構

  • 分享至 

  • xImage
  •  

這篇開始,進入第三單元「Android 的架構」。在上個單元,我們雖說了要儘量用單元測試的方式,但其實要做起來還是有點困難的,這是因為Activity經常有著過多的邏輯,導至測試不易。

出版書:
Android TDD 測試驅動開發:從 UnitTest、TDD 到 DevOps 實踐

在第三單元,將介紹以下:

  • MVP的架構及單元測試。
  • MVVP的架構及寫單元測試。
  • APP加上呼叫WebAPI取得資料的功能、非同步的議題。

這篇首先要介紹的是MVP的架構,MVP將內容從呈現(Presenter)和資料處理(Model)與內容(View)分開。

在MVC的架構,通常會把layout(xml)當成View,Activity當成Controller。事實上,Activity 卻是Controller 與View的混合,於是Activity既要做處理View,也負責商業邏輯。使得Activity越來越肥。
MVC 與 MVP 的最大差異在於MVP把Activity的商業邏輯移到Presenter,Activity 專心於View
MVP:

  • Model - 管理資料來源。例:SharedPreferences、Room、呼叫API
  • View - 顯示UI和與使用者互動I,如 Activity、Fragment
  • Presenter - 負責邏輯處理

範例:
這是一個商品的頁面,上面的資料是跟WebAPI取得商品資料(商品名稱、螢幕大小、售價)

https://ithelp.ithome.com.tw/upload/images/20191001/20111896e3HQPfS0OX.png

建立ProductActivity 為MVP 中的View。
建立ProductContract ,裡面放了IProductView、IProductPresenter 2個Interface。
建立ProductPresenter,負責商業邏輯,與Model互動。
建立ProductRepository,負責取得商品資料。

https://ithelp.ithome.com.tw/upload/images/20191001/20111896eiTQNxSUsp.png

Model

首先是Model,也就是Repository,建立一個IPoroductRepository的Interface。

interface IProductRepository {
    //傳入商品編號,取得商品資料
    fun getProduct(productId: String, loadProductCallback: LoadProductCallback)

    interface LoadProductCallback {
        //回傳商品資料Response
        fun onProductResult(productResponse: ProductResponse)
    }
}

實作ProductRepository.getProduct。ProductRepository的建購子傳入productAPI,這是用來模擬API取得資料。

class ProductRepository(private val productAPI: IProductAPI) : IProductRepository {

    override fun getProduct(productId: String, loadProductCallback: IProductRepository.LoadProductCallback) {
        productAPI.getProduct(productId, object : IProductAPI.ProductDataCallback {
            override fun onGetResult(productResponse: ProductResponse) {
                loadProductCallback.onProductResult(productResponse)
            }
        })
    }
}

新增ProductAPI,用來模擬取得WebAPI的產品資料

interface IProductAPI {
    interface ProductDataCallback {
        fun onGetResult(productResponse: ProductResponse)
    }

    fun getProduct(productId:String, ProductDataCallback: ProductDataCallback)
}

class ProductAPI: IProductAPI {

    override fun getProduct(productId:String, loadAPICallBack: IProductAPI.ProductDataCallback) {
        //模擬從API取得資料
        val handler = Handler()
        handler.postDelayed(Runnable {
            val productResponse = ProductResponse()
            productResponse.id = "pixel3"
            productResponse.name = "Google Pixel 3"
            productResponse.desc = "5.5吋螢幕"
            productResponse.price = 27000
            callback.onGetResult(productResponse)
        }, 1000)
    }
}

商品資料的Model,這個Response就是用來將WebAPI回傳的資料存到這個DataModel

class ProductResponse {
     lateinit var id: String
     lateinit var name: String
     lateinit var desc: String
     var price: Int = 0
}

到目前為止,我們完成了MVP裡Model的部分,ProductRepository負責跟ServiceAPI取得商品資料

Contract ( Interface)

MVP的架構會有一個Contract的類別,裡面是定義View與Presenter之間的互動:
1.Activity 呼叫Presenter的Interface
2.Presenter callback的Interface

class ProductContract {

    interface IProductPresenter {
        //取得商品資料
        fun getProduct(productId: String)
    }

    interface IProductView {
        //取得資料的Callback
        fun onGetResult(productResponse: ProductResponse)
    }
}

Presenter

Presenter 實作 IProductPresenter,這裡的建構子必須傳入ProductContract.IProductView,當Presenter跟Repository取得資料時,會呼叫ProductContract.IProductView.onGetResult通知View通新畫面。

https://ithelp.ithome.com.tw/upload/images/20191001/201118961e6SsCgvza.png

View(Activity)

Activity負責2件事
1.跟Presenter要資料
2.實作IProductView.onProductResult 將商品Response放至UI上

1.跟Presenter要資料,在這個步驟,View必須將自已傳給ProductPresenter,讓ProductPresenter跟Repository取得資料後可以callback要求View顯示資料。

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    val productRepository = ProductRepository(ProductAPI())
    // view必須將自已傳給Presenter,也就是this
    val productPresenter = ProductPresenter(this, productRepository)

    //向Presenter取得資料
    productPresenter.getProduct(productId)
}

2.實作IProductView.onProductResult 將商品Response放至UI上

//實作IProductView.onGetResult
override fun onGetResult(productResponse: ProductResponse) {
    //將商品Response放到View上
    productName.text = productResponse.name
    productDesc.text = productResponse.desc

    val currencyFormat = NumberFormat.getCurrencyInstance()
    currencyFormat.maximumFractionDigits = 0
    val price = currencyFormat.format(productResponse.price)
    productPrice.text = price
}

可以看到View被分割的很乾淨,只負責跟Presenter取資料、更新ProductResponse的資料到View

https://ithelp.ithome.com.tw/upload/images/20191001/20111896fFhNKUZNbP.png

這樣MVP的架構就完成了,給大家一個練習,這個畫面下方有一個「購買」的按鈕。按下購買後,如購買成功Toast「購買成功」,購買失敗則Alert「購買失敗」應該怎麼寫。答案在範例下載。

下一篇將介紹MVP架構下的單元測試。

範例下載:
https://github.com/evanchen76/MVPUnitTestSample

給Android 初學者 的快速成長 線上課程

1️⃣ UI 進階實戰 — Material Design Component 讓你簡單做出效果超好的UI

2️⃣ 動畫入門到進階 — 用動畫提升使用者體驗

3️⃣ 架構設計 — MVP、MVVM 讓你程式碼好維護

1️⃣ + 2️⃣ + 3️⃣ 3堂組合包更划算 — Android 架構設計 + 動畫入門到進階 + UI 進階實戰


上一篇
Day16 - Android 測試小結
下一篇
Day18 - Android MVP 架構的單元測試
系列文
Android TDD 測試驅動開發30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言